/*
 * ATM Example system - file SimulatedBank.java 
 *
 * copyright (c) 2001 - Russell C. Bjork
 *
 */
 
package simulation;
import banking.AccountInformation;
import banking.Balances;
import banking.Card;
import banking.Message;
import banking.Money;
import banking.Status;

/** Simulation of the bank.  A set of simulated accounts is initalized at startup.
 */
public class SimulatedBank
{
    /** Simulate the handling of a message
     *
     *  @param message the message to send
     *  @param balances (out) balances in customer's account as reported
     *         by bank
     *  @return status code returned by bank
     */
    public Status handleMessage(Message message, Balances balances)
    {       
        int cardNumber = message.getCard().getNumber(); 
        if (cardNumber < 1 || cardNumber > PIN.length)
            return new Failure("Invalid card");
    
        if (message.getPIN() != PIN [ cardNumber ] )
            return new InvalidPIN();
    
        switch(message.getMessageCode())
        {
            case Message.WITHDRAWAL:
            
                return withdrawal(message, balances);
                            
            case Message.INITIATE_DEPOSIT:
            
                return initiateDeposit(message);
            
            case Message.COMPLETE_DEPOSIT:
            
                return completeDeposit(message, balances);
            
            case Message.TRANSFER:
            
                return transfer(message, balances);
            
            case Message.INQUIRY:
            
                return inquiry(message, balances);
        }
        
        // Need to keep compiler happy
        
        return null;
    }

    /** Simulate processing of a withdrawal
     *
     *  @param message the message describing the withdrawal requested
     *  @param balances (out) balances in account after withdrawal
     *  @return status code derived from current values
     */
    private Status withdrawal(Message message, Balances balances)
    {
        int cardNumber = message.getCard().getNumber();
        
        int accountNumber = ACCOUNT_NUMBER [ cardNumber ] [ message.getFromAccount() ];
        if (accountNumber == 0)
            return new Failure("Invalid account type");
    
        Money amount = message.getAmount();
        
        Money limitRemaining = new Money(DAILY_WITHDRAWAL_LIMIT);
        limitRemaining.subtract(WITHDRAWALS_TODAY[ cardNumber ]);
        if (! amount.lessEqual(limitRemaining))
            return new Failure("Daily withdrawal limit exceeded");

        if (! amount.lessEqual(AVAILABLE_BALANCE [ accountNumber ]))
             return new Failure("Insufficient available balance");

        // Update withdrawals today and account balances once we know everything is
        // OK
            
        WITHDRAWALS_TODAY [ cardNumber ].add(amount);
        BALANCE [ accountNumber ].subtract(amount);
        AVAILABLE_BALANCE [ accountNumber ].subtract(amount);
        
        // Return updated balances
        
        balances.setBalances(BALANCE [ accountNumber ], 
                             AVAILABLE_BALANCE [ accountNumber ]);
        
        return new Success();
    } 
    
    /** Simulate initiation of a deposit. At this point, the bank only approves
     *  the validity of the deposit - no changes to the records are made until
     *  the envelope is actually inserted 
     *
     *  @param message the message describing the deposit requested
     *  @return status code derived from current values
     */
    private Status initiateDeposit(Message message)
    {
        int cardNumber = message.getCard().getNumber(); 
    
        int accountNumber = ACCOUNT_NUMBER [ cardNumber ] [ message.getToAccount() ];
        if (accountNumber == 0)
            return new Failure("Invalid account type");
            
        // Don't update anything yet
            
        return new Success();
    }    
    
    /** Simulate completion of a deposit
     *
     *  @param message the message describing the deposit requested
     *  @param balances (out) balances (not updated until completed)
     *  @return status code - must always be success in this case
     */
    private Status completeDeposit(Message message, Balances balances)
    {
        int cardNumber = message.getCard().getNumber(); 
        
        int accountNumber = ACCOUNT_NUMBER [ cardNumber ] [ message.getToAccount() ];
        if (accountNumber == 0)
            return new Failure("Invalid account type");
            
        // Now we can update the balance
        
        Money amount = message.getAmount();
        BALANCE [ accountNumber ].add(amount);
        
        // Return updated balances
        
        balances.setBalances(BALANCE [ accountNumber ], 
                             AVAILABLE_BALANCE [ accountNumber ]);
        
        return new Success();
    }    
    
    /** Simulate processing of a transfer
     *
     *  @param message the message describing the transfer requested
     *  @param balances (out) balances in "to" account after transfer
     *  @return status code derived from current values
     */
    private Status transfer(Message message, Balances balances)
    {
        int cardNumber = message.getCard().getNumber(); 
    
        int fromAccountNumber = ACCOUNT_NUMBER [ cardNumber ] [ message.getFromAccount() ];
        if (fromAccountNumber == 0)
            return new Failure("Invalid from account type");
    
        int toAccountNumber = ACCOUNT_NUMBER [ cardNumber ] [ message.getToAccount() ];
        if (toAccountNumber == 0)
            return new Failure("Invalid to account type");
        if (fromAccountNumber == toAccountNumber)
            return new Failure("Can't transfer money from\n" +
                    "an account to itself");
    
        Money amount = message.getAmount();
        
        if (! amount.lessEqual(AVAILABLE_BALANCE [ fromAccountNumber ]))
             return new Failure("Insufficient available balance");

        // Update account balances once we know everything is OK
            
        BALANCE [ fromAccountNumber ].subtract(amount);
        AVAILABLE_BALANCE [ fromAccountNumber ].subtract(amount);
        BALANCE [ toAccountNumber ].add(amount);
        AVAILABLE_BALANCE [ toAccountNumber ].add(amount);
        
        // Return updated balances
        
        balances.setBalances(BALANCE [ toAccountNumber ], 
                             AVAILABLE_BALANCE [ toAccountNumber ]);
        
        return new Success();
    } 
    
    /** Simulate processing of an inquiry
     *
     *  @param message the message describing the inquiry requested
     *  @param balances (out) balances in account
     *  @return status code derived from current values
     */
    private Status inquiry(Message message, Balances balances)
    {
        int cardNumber = message.getCard().getNumber(); 

        int accountNumber = ACCOUNT_NUMBER [ cardNumber ] [ message.getFromAccount() ];
        if (accountNumber == 0)
            return new Failure("Invalid account type");
        
        // Return requested balances
        
        balances.setBalances(BALANCE [ accountNumber ], 
                             AVAILABLE_BALANCE [ accountNumber ]);
        
        return new Success();
    }
     
    /** Representation for status of a transaction that succeeded
     */
    private static class Success extends Status
    {
        public boolean isSuccess()
        {
            return true;
        }
        
        public boolean isInvalidPIN()
        {
            return false;
        }
        
        public String getMessage()
        {
            return null;
        }
    }
    
    /** Representation for status of a transaction that failed (for reason other than
     *  invalid PIN)
     */
    private static class Failure extends Status
    {
        /** Constructor
         *
         *  @param message description of the failure
         */
        public Failure(String message)
        {
            this.message = message;
        }
        
        public boolean isSuccess()
        {
            return false;
        }
        
        public boolean isInvalidPIN()
        {
            return false;
        }
        
        public String getMessage()
        {
            return message;
        }
        
        private String message;
    }

    /** Representation for status of a transaction that failed due to an invalid PIN
     */
    private static class InvalidPIN extends Failure
    {
        /** Constructor
         *
         *  @param message description of the failure
         */
        public InvalidPIN()
        {
            super("Invalid PIN");
        }
        
        public boolean isInvalidPIN()
        {
            return true;
        }
    }
    
    /** PIN for each card.  (Valid card numbers start with 1)
     */
    private static final int PIN [] =
    { 
        0,  // dummy for nonexistent card 0
        42, 
        1234 
    };

    /** Array of account numbers associated with each card.  For each card,
     *  there can be three different types of account, which correspond to
     *  the names in class AccountInformation.  0 means no account of this
     *  type.   (Valid card numbers start with 1)
     */
    private static final int ACCOUNT_NUMBER [] [] =
    { 
        { 0, 0, 0 },    // dummies for nonexistent card 0
        { 1, 2, 0 },
        { 1, 0, 3 }
    };

    /** Withdrawals so far today on each card.   (Valid card numbers start with 1)
     */ 
    private static Money WITHDRAWALS_TODAY [] =
    {
        new Money(0),   // dummy for nonexistent card 0
        new Money(0),
        new Money(0)
    };
    
    /** Maximum daily withdrawal limit for any one card.  
     */
    private static final Money DAILY_WITHDRAWAL_LIMIT = new Money(300);
    
    /** Balance for each account (will change as program runs, hence not a
     *  static final.
     */
    private Money BALANCE [] =
    {
        new Money(0),   // dummy for nonexistent account 0
        new Money(100), 
        new Money(1000),
        new Money(5000) 
    }; 
    
    /** Available alance for each account (will change as program runs, hence
     *  not a static final.
     */
    private Money AVAILABLE_BALANCE [] =
    { 
        new Money(0),   // dummy for nonexistent account 0
        new Money(100), 
        new Money(1000),
        new Money(5000) 
    };
}    